feat(resume): Resume Pipeline — "continue exactly where you left off"#51
Conversation
…eft off" Reopening a session now revalidates the repository against the state the session last saw and hands the agent a structured repository delta, so it resumes against verified repository reality instead of a stale transcript. The Claude Agent SDK persists the conversation (options.resume) but not filesystem/repository state; this platform service (owned by the app, like Memory/Search) closes that gap. Main process: - ResumeManager + delta.ts: per-session repository snapshots (HEAD + branch + content-free dirty hash), activation-time revalidation with a cheap short-circuit and a bounded deadline, and an argv-only git delta (ahead/behind, capped commit list, name-status merged with the dirty set, category flags incl. limboo.json, history-rewrite and root-change detection). Never blocks session switching. - Schema v10: session_snapshots + resume_deltas (persisted so a pending one-shot injection survives a restart). - Code intelligence (schema v11): search_files.content_hash skips unchanged files in incremental indexing; new search_refs regex import-edge table (parser-agnostic) powers importer counts; per-file symbol adds/removes diffed across a reindex. - Memory revalidation: memory_links now written on create/acceptProposal; memories whose linked files vanish are confidence-downgraded (x0.6, floor 0.1) and restored when the file returns. - Injection: AgentManager.resumeContextFor is a third context producer rendering a one-shot <repository-delta> block, cached on the run record for recovery retries. - IPC: resume:getState/getDelta/dismiss/revalidate (string ids only; revalidate gated to the active session) + resume:state-changed; preload window.limboo.resume. Renderer (existing UI idioms only, pure-black tokens): - ResumeBanner, a "Revalidating..." header chip, ResumeDeltaDialog, useResumeStore; settings under Memory & Search (settings.resume, RESUME_LIMITS). Docs: deep subsystem doc (docs/architecture/subsystems/resume-pipeline.md), docs index + README updates, and a CLAUDE.md section. Verified: lint clean, renderer build + main/preload esbuild bundles clean, 25 delta assertions against a real git repo, and DDL/queries against a live SQLite engine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
This PR implements a comprehensive Resume Pipeline feature that enables developers to resume development sessions with full awareness of repository changes that occurred while the session was suspended. The implementation is well-architected with proper security controls and error handling.
Key Strengths:
- Parameterized SQL queries throughout - all values are bound via placeholders, never concatenated
- Git operations use argv-only invocations through the shared runner (no shell execution)
- Proper input validation with regex guards before git commands
- All data structures are bounded by RESUME_LIMITS constants
- Graceful degradation on failures - never blocks session operations
- Comprehensive database schema with appropriate indexes
The code follows all security best practices documented in CLAUDE.md §6 and is ready for merge.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
📝 WalkthroughWalkthroughThis PR introduces a "Resume Pipeline" that snapshots per-session repository state, computes and persists structured repository deltas, enriches them via Search and Memory subsystems, injects a repository-delta block into agent prompts, and exposes IPC endpoints, a renderer store, UI (banner/dialog), settings, and documentation for the new subsystem. ChangesResume Pipeline
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/index.ts (1)
262-293: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDefer resume revalidation until after worktree recovery
worktrees.recover()can broadcast session changes beforeresume.onBoot()runs, andResumeManager.onActiveSessionChanged()will revalidate immediately whileprevActiveIdis stillnull. That can publish a delta against a worktree state that is still settling. Gate the listener until after boot, or initializeprevActiveIdbefore recovery starts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/index.ts` around lines 262 - 293, The resume listener is revalidating too early because sessions.onActiveChanged(() => resume.onActiveSessionChanged(active)) can fire during worktree recovery before ResumeManager is bootstrapped. Update the boot sequence in src/main/index.ts so ResumeManager.onActiveSessionChanged only runs after worktrees.recover() has settled, or initialize the resume state (including prevActiveId) before recovery starts; use the workspace/worktrees/sessions/resume hooks in the existing startup flow to locate the change.
🧹 Nitpick comments (2)
src/main/managers/resume/ResumeManager.ts (1)
315-329: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winClear the timeout and note that the timed-out revalidation keeps running.
Promise.raceleaves thesetTimeouttimer pending on the happy path (it fires up torevalidateTimeoutMslater for a promise nothing awaits). More importantly, losing the race does not cancelrevalidateInner— after the catch sets phase'idle', the still-running inner call can latersetState('checking')→publishDelta/setState('delta'), so a delta may surface after the "degraded to no-delta" state was already broadcast. At minimum clear the timer; true cancellation would require threading anAbortSignalinto the git calls.♻️ Clear the timer in a finally block
async revalidate(sessionId: string): Promise<void> { const cfg = this.settings.getAll().resume; if (!cfg.enabled) return; + let timer: NodeJS.Timeout | undefined; try { await Promise.race([ this.revalidateInner(sessionId, cfg.maxCommitsInDelta, cfg.staleThresholdDays), - new Promise<never>((_, reject) => - setTimeout(() => reject(new Error('revalidate timeout')), RESUME_LIMITS.revalidateTimeoutMs), - ), + new Promise<never>((_, reject) => { + timer = setTimeout( + () => reject(new Error('revalidate timeout')), + RESUME_LIMITS.revalidateTimeoutMs, + ); + }), ]); } catch (err) { logger.warn('resume: revalidation degraded to no-delta', err); this.setState({ sessionId, phase: 'idle' }); + } finally { + if (timer) clearTimeout(timer); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/managers/resume/ResumeManager.ts` around lines 315 - 329, The revalidation flow in ResumeManager.revalidate leaves the timeout pending and allows revalidateInner to keep running after the race is lost. Update the Promise.race setup so the timeout from setTimeout is always cleared in a finally block, and make it clear in the code path that a timed-out revalidation is only degraded, not canceled. Use the revalidate, revalidateInner, and RESUME_LIMITS.revalidateTimeoutMs symbols to locate the fix.src/renderer/features/resume/ResumeBanner.tsx (1)
17-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFragile text-parsing for severity styling.
warnis derived by regex-matchingstate.summary, a human-readable string. This silently breaks if the summary wording in the backend (delta/ResumeManager) ever changes, since there's no compile-time coupling between the two.Consider adding a structured flag (e.g.,
severity: 'warning' | 'info'or mirroringhistoryRewritten/rootChanged) toResumeStateso the UI doesn't need to sniff free text.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/features/resume/ResumeBanner.tsx` around lines 17 - 32, The severity styling in ResumeBanner is currently inferred by regex-matching state.summary, which is fragile. Update ResumeState and the delta/ResumeManager flow to carry a structured severity field (or explicit flags like historyRewritten/rootChanged), then have ResumeBanner use that typed value instead of parsing free text for the AlertTriangle/Info choice.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/db/database.ts`:
- Around line 199-229: Purge the resume state when a session is deleted:
`SessionManager.purge` currently removes the session row and agent data but
leaves `session_snapshots` and `resume_deltas` behind. Update the purge flow in
`SessionManager.purge` to also delete rows for the same session_id from both
tables, keeping all session-scoped resume data in sync with the session
lifecycle.
In `@src/main/managers/search/refs.ts`:
- Around line 42-46: The ref extraction loop in refs.ts is skipping every line
that starts with # before CFAMILY_RULES can match `#include` directives, so C/C++
include edges never get produced. Update the comment-filter logic in the loop
that scans lines so it still ignores shell/Python-style comments but allows
`#include` lines to reach the existing CFAMILY_RULES matching logic, keeping the
behavior in the search/ref extraction path aligned with C-family include
handling.
---
Outside diff comments:
In `@src/main/index.ts`:
- Around line 262-293: The resume listener is revalidating too early because
sessions.onActiveChanged(() => resume.onActiveSessionChanged(active)) can fire
during worktree recovery before ResumeManager is bootstrapped. Update the boot
sequence in src/main/index.ts so ResumeManager.onActiveSessionChanged only runs
after worktrees.recover() has settled, or initialize the resume state (including
prevActiveId) before recovery starts; use the
workspace/worktrees/sessions/resume hooks in the existing startup flow to locate
the change.
---
Nitpick comments:
In `@src/main/managers/resume/ResumeManager.ts`:
- Around line 315-329: The revalidation flow in ResumeManager.revalidate leaves
the timeout pending and allows revalidateInner to keep running after the race is
lost. Update the Promise.race setup so the timeout from setTimeout is always
cleared in a finally block, and make it clear in the code path that a timed-out
revalidation is only degraded, not canceled. Use the revalidate,
revalidateInner, and RESUME_LIMITS.revalidateTimeoutMs symbols to locate the
fix.
In `@src/renderer/features/resume/ResumeBanner.tsx`:
- Around line 17-32: The severity styling in ResumeBanner is currently inferred
by regex-matching state.summary, which is fragile. Update ResumeState and the
delta/ResumeManager flow to carry a structured severity field (or explicit flags
like historyRewritten/rootChanged), then have ResumeBanner use that typed value
instead of parsing free text for the AlertTriangle/Info choice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: dfb7225d-db3e-4f7f-b09e-d3b6f29de480
📒 Files selected for processing (28)
CLAUDE.mdREADME.mddocs/README.mddocs/architecture/subsystems/resume-pipeline.mdsrc/main/db/database.tssrc/main/index.tssrc/main/ipc/index.tssrc/main/ipc/memoryHandlers.tssrc/main/ipc/resumeHandlers.tssrc/main/managers/AgentManager.tssrc/main/managers/GitManager.tssrc/main/managers/SettingsManager.tssrc/main/managers/memory/MemoryManager.tssrc/main/managers/resume/ResumeManager.tssrc/main/managers/resume/delta.tssrc/main/managers/search/SearchManager.tssrc/main/managers/search/refs.tssrc/preload/index.tssrc/renderer/App.tsxsrc/renderer/features/resume/ResumeBanner.tsxsrc/renderer/features/resume/ResumeDeltaDialog.tsxsrc/renderer/features/settings/catalog.tsxsrc/renderer/features/settings/panels/MemoryPanel.tsxsrc/renderer/features/workspace/CenterWorkspace.tsxsrc/renderer/stores/useResumeStore.tssrc/shared/constants.tssrc/shared/ipc-channels.tssrc/shared/types.ts
| -- Resume Pipeline (schema v10) — one repository anchor per session, | ||
| -- upserted at meaningful moments (run end, checkpoint, deactivation). | ||
| -- head/branch are NULL when the effective root is not a git repo (or the | ||
| -- branch is detached). dirty_files is a capped JSON array of {path,status}; | ||
| -- dirty_hash is a sha256 over the sorted dirty entries | ||
| -- (status|path|size|mtimeMs) so content drift in an already-dirty tree is | ||
| -- detected without ever reading file contents. All values bound. | ||
| CREATE TABLE IF NOT EXISTS session_snapshots ( | ||
| session_id TEXT PRIMARY KEY, | ||
| workspace_id TEXT NOT NULL, | ||
| root TEXT NOT NULL, | ||
| head TEXT, | ||
| branch TEXT, | ||
| dirty_hash TEXT NOT NULL DEFAULT '', | ||
| dirty_files TEXT NOT NULL DEFAULT '[]', | ||
| reason TEXT NOT NULL, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL | ||
| ); | ||
|
|
||
| -- The last computed repository delta per session, persisted so the | ||
| -- one-shot prompt injection survives an app restart between detection and | ||
| -- the next prompt. status: 'pending' | 'injected' | 'dismissed'. | ||
| -- delta is a JSON RepoDelta built entirely in the main process. | ||
| CREATE TABLE IF NOT EXISTS resume_deltas ( | ||
| session_id TEXT PRIMARY KEY, | ||
| status TEXT NOT NULL, | ||
| delta TEXT NOT NULL, | ||
| created_at INTEGER NOT NULL | ||
| ); | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for deletion/cleanup of the new resume tables across the codebase.
rg -nP "session_snapshots|resume_deltas" --type=ts -C3
# Locate the session hard-delete path to confirm it cascades to resume rows.
rg -nP "DELETE FROM sessions|deleteSession|purge|hardDelete" --type=ts -C3Repository: BotCoder254/limboo
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map likely TypeScript files and search for session deletion/cleanup paths.
git ls-files 'src/**/*.ts' | sed -n '1,250p'
printf '\n## session_snapshots / resume_deltas references\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' 'session_snapshots|resume_deltas' src -C 3 || true
printf '\n## session deletion / trashing references\n'
rg -n --hidden --glob '!**/dist/**' --glob '!**/build/**' --glob '!**/node_modules/**' 'deleteSession|hardDelete|trash|purge|DELETE FROM sessions|FROM sessions' src -C 3 || trueRepository: BotCoder254/limboo
Length of output: 37931
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## SessionManager.purge and surrounding context\n'
sed -n '330,352p' src/main/managers/SessionManager.ts
printf '\n## sessionHandlers delete/purge paths\n'
sed -n '180,230p' src/main/ipc/sessionHandlers.ts
printf '\n## ResumeManager methods around snapshot/delta lifecycle\n'
sed -n '140,180p' src/main/managers/resume/ResumeManager.ts
sed -n '250,360p' src/main/managers/resume/ResumeManager.ts
sed -n '380,540p' src/main/managers/resume/ResumeManager.tsRepository: BotCoder254/limboo
Length of output: 15119
Purge the resume tables with the session
SessionManager.purge deletes the session row and agent data, but session_snapshots and resume_deltas are left behind. Add matching deletes here so permanently removed sessions don't leave orphaned resume state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/db/database.ts` around lines 199 - 229, Purge the resume state when
a session is deleted: `SessionManager.purge` currently removes the session row
and agent data but leaves `session_snapshots` and `resume_deltas` behind. Update
the purge flow in `SessionManager.purge` to also delete rows for the same
session_id from both tables, keeping all session-scoped resume data in sync with
the session lifecycle.
| for (let i = 0; i < lines.length; i += 1) { | ||
| if (out.length >= SEARCH_LIMITS.maxRefsPerFile) break; | ||
| const raw = lines[i]; | ||
| const trimmed = raw.trimStart(); | ||
| if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('#')) continue; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
C/C++ #include directives are never extracted.
The comment guard at Line 46 skips any line that starts with # (to drop Python/shell comments), but CFAMILY_RULES (Line 99) matches exactly #include <...> / #include "...". Since those lines start with #, they are dropped before the rules run, so c/cpp include edges are silently never produced and CFAMILY_RULES is effectively dead code.
Narrow the # skip so include directives survive:
🐛 Proposed fix
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('#')) continue;
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
+ // `#` is a comment leader (Python/Ruby/shell) but also a C-family `#include`.
+ if (trimmed.startsWith('#') && !/^#\s*include\b/.test(trimmed)) continue;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (let i = 0; i < lines.length; i += 1) { | |
| if (out.length >= SEARCH_LIMITS.maxRefsPerFile) break; | |
| const raw = lines[i]; | |
| const trimmed = raw.trimStart(); | |
| if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('#')) continue; | |
| for (let i = 0; i < lines.length; i += 1) { | |
| if (out.length >= SEARCH_LIMITS.maxRefsPerFile) break; | |
| const raw = lines[i]; | |
| const trimmed = raw.trimStart(); | |
| if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue; | |
| // `#` is a comment leader (Python/Ruby/shell) but also a C-family `#include`. | |
| if (trimmed.startsWith('#') && !/^#\s*include\b/.test(trimmed)) continue; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/managers/search/refs.ts` around lines 42 - 46, The ref extraction
loop in refs.ts is skipping every line that starts with # before CFAMILY_RULES
can match `#include` directives, so C/C++ include edges never get produced. Update
the comment-filter logic in the loop that scans lines so it still ignores
shell/Python-style comments but allows `#include` lines to reach the existing
CFAMILY_RULES matching logic, keeping the behavior in the search/ref extraction
path aligned with C-family include handling.
Summary
Reopening a session now revalidates the repository against the state the session last saw and hands the agent a structured repository delta, so it resumes against verified repository reality instead of a stale transcript. The Claude Agent SDK persists the conversation (
options.resume) but explicitly not filesystem/repository state; this platform service — owned by the app, like Memory and Search — closes that gap. Fully local, bounded argv-only git, and it never blocks session switching.Deep dive:
docs/architecture/subsystems/resume-pipeline.md.What's included
Main process
ResumeManager+delta.ts— per-session repository snapshots (HEAD + branch + a content-free dirty hash vialstat), activation-time revalidation with a cheap short-circuit and a 10s deadline (degrades to "no delta" on any failure), and an argv-only repository delta: ahead/behind counts, capped commit list,--name-statusmerged with the dirty set, category flags (manifests/lockfiles/limboo.json/migrations), plus history-rewrite and root-change detection.session_snapshots+resume_deltas(persisted so a pending one-shot injection survives a restart).search_files.content_hashskips unchanged files in incremental indexing (no FTS churn); a new parser-agnosticsearch_refsimport-edge table powers importer counts; per-file symbol adds/removes are diffed across a reindex. No tree-sitter, no embeddings.memory_linksis now written oncreate/acceptProposal; memories whose linked files vanish are confidence-downgraded (×0.6, floor 0.1) and restored when the file returns.AgentManager.resumeContextForis a third context producer rendering a one-shot<repository-delta>block, cached on the run record so recovery retries re-inject the same block.resume:getState/getDelta/dismiss/revalidate(string session ids only;revalidategated to the active session) +resume:state-changed; preloadwindow.limboo.resume.Renderer (existing UI idioms only, pure-black tokens)
ResumeBanner(missing-worktree-banner idiom), a "Revalidating…" header chip,ResumeDeltaDialog(hooks-confirm idiom),useResumeStore; settings under Memory & Search (settings.resume,RESUME_LIMITS).Docs — deep subsystem doc, docs index + README updates, and a
CLAUDE.mdsection.Architecture notes
sessions.onActiveChangedlistener, so the existing effective-root retarget path is untouched. Boot revalidation chains strictly after worktree recovery.options.resumeor the corrupted-resume self-heal — the delta ridessystemPrompt.appendbeside the memory/search blocks.Verification
npm run lintclean; renderervite build+main/preloadesbuild bundles clean.--porcelain=v1 -zparsing, ahead/behind, commit list, dirty-merge, manifest flags, fast-forward vs. history-rewrite, block budget, ref resolution incl. traversal rejection, symbol extraction).content_hash, importer-count query, snapshot/delta upserts, memory-downgrade JOIN).Security
Argv-only git (snapshot HEAD regex-validated before entering any argv), parameterized SQL only,
isInsideRoot-guardedlstat, string-id-only IPC (no prototype-pollution surface), memory link paths validated/normalized, and no commit subjects/paths in logs.🤖 Generated with Claude Code
Note
Medium Risk
Touches agent prompt composition, session activation git work, and DB schema v11 migrations; failures are designed to degrade to no delta and not block switching, but incorrect deltas could mislead the agent.
Overview
Introduces the Resume Pipeline so reopening a session compares the worktree to a persisted repository snapshot (HEAD, branch, content-free dirty hash) instead of relying only on SDK conversation resume.
Main process: New
ResumeManageranddelta.tsupsertsession_snapshotsandresume_deltas(schema v10), run deadline-bounded revalidation on session switch and post–worktree-recovery boot, and compute argv-only git deltas (commits, files, manifests/migrations, history rewrite / root change).AgentManageradds a thirdsystemPrompt.appendproducer (<repository-delta>), one-shot per delta with run-level cache for recovery retries, plus snapshots at run end;GitManagerre-anchors on checkpoints. Search gainscontent_hashskip-on-unchanged indexing,search_refsimport edges, and APIs for symbol diff and importer counts (schema v11). Memory persistsmemory_linkson create (with optionalfilePath/symbolRefsfrom IPC) and downgrades/restores confidence when linked files disappear or return.Bridge & UI:
window.limboo.resume, resume IPC handlers,useResumeStore,ResumeBanner, header “Revalidating…” chip, andResumeDeltaDialog; Memory & Search settings forsettings.resumeandRESUME_LIMITS. Docs/README/CLAUDE updated for the subsystem.Reviewed by Cursor Bugbot for commit 20375e3. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes